﻿using System;
using System.Configuration;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using Cubicle.Core.DataContracts;
using Mono.Cecil;
using NLog;

namespace Cubicle.Core.Shared
{
    public static class Helpers
    {
        // Nlog
        private static Logger _logger = NLog.LogManager.GetCurrentClassLogger();

        // Default parameters
        public const string BinDirectory = "bin";

        #region Help

        public static string Help(this Type objType)
        {
            var rop = Attribute.GetCustomAttribute(objType, typeof(Help));
            return rop == null
                ? null
                : ((Help)rop).Description;
        }

        public static string Help(this object obj)
        {
            return obj.GetType().Help();
        }

        public static string Help(this MethodBase mi)
        {
            var a = Attribute.GetCustomAttribute(mi, typeof(Help));
            return a == null
                ? null
                : ((Help)a).Description;
        }

        public static string Help(this Type objType, string method)
        {
            var methodInfo = objType.GetMethods().Where(a => a.Name == method).FirstOrDefault();
            if (methodInfo == null) return null;
            return methodInfo.Help();
        }

        public static string Help(this object obj, string method)
        {
            return obj.GetType().Help(method);
        }

        public static string Help(this ParameterInfo info)
        {
            var attrib = Attribute.GetCustomAttribute(info, typeof (Help));
            return attrib == null 
                ? null 
                : ((Help) attrib).Description;
        }

        #endregion

        #region Logger extensions

        public static void DebugException(this Logger logger, Exception e, string message = null, params object[] args)
        {
            logger.DebugException(String.Format(message ?? "", args), e);
        }

        public static void ErrorException(this Logger logger, Exception e, string message = null, params object[] args)
        {
            logger.ErrorException(String.Format(message ?? "", args), e);
        }

        public static IEnumerable<string> GetMemoryLog(int i = 0)
        {
            // Retrieve all memory logs
            var targets = LogManager.Configuration.AllTargets
                .OfType<NLog.Targets.MemoryTarget>();

            // If no memory logs, return
            if (targets.Count() == 0) return null;

            // Retrieve all items and order them
            var result = targets
                .SelectMany(x => x.Logs)
                .OrderByDescending(x => x);

            // Only take as many as needed
            return i > 0
                ? result.Take(i).ToList()
                : result.ToList();
        }

        public static LogMessageGenerator Format(this string str, params object[] pars)
        {
            return () => String.Format(str, pars);
        }

        #endregion

        #region Config

        public static string BinPath
        {
            get
            {
                var binPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, BinDirectory);
                if (Directory.Exists(binPath)) return binPath;
                return null;
            }
        }

        public static Configuration ConfigFile(string configFile)
        {
            var configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, configFile);
            if (!File.Exists(configPath)) configPath = Path.Combine(Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory + @"\.."), configFile);
            if (!File.Exists(configPath)) return null;
            var map = new ExeConfigurationFileMap();
            map.ExeConfigFilename = configPath;
            return ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);            
        }

        //public static CortexConfigurationSection CortexConfigSection(string configFile = DefaultConfigFileName)
        //{
        //     try
        //     {
        //         var config = ConfigFile(configFile);
        //         var configSection = (CortexConfigurationSection)config.GetSection(CortexConfigurationSection.SectionId);
        //         return configSection;
        //     }
        //     catch (Exception e)
        //     {
        //         _logger.ErrorException(e, "Error reading cortex configuration section");
        //         return null;
        //     }
        //}

        public static string GetKey(this Configuration config, string key, string defaultValue = null)
        {
            return config.AppSettings.Settings.AllKeys.Contains(key)
                       ? config.AppSettings.Settings[key].Value.ToString()
                       : defaultValue;
        }

        public static string GetKey(string key, string config, string defaultValue = null)
        {
            try
            {
                return ConfigFile(config).GetKey(key, defaultValue);
            } catch (Exception)
            {
                return defaultValue;
            }
        }

        #endregion

        #region Reflection 

        public static IEnumerable<AssemblyData> GetAssemblyDetails()
        {
            return AppDomain
                .CurrentDomain
                .GetAssemblies()
                .Select(x => new AssemblyData(x));
        }

        public static IEnumerable<string> GetDlls(string path)
        {
            return Directory
                .GetFiles(path)
                .Where(p => Path.GetExtension(p) == ".dll" || Path.GetExtension(p) == ".exe");
        }

        public static IEnumerable<Type> SafeGetTypes(
            this Assembly asm, 
            out List<string> missingDependencies, 
            bool logErrors = false)
        {
            IEnumerable<Type> types;
            missingDependencies = null;
            try
            {
                types = asm.GetTypes();
                if (logErrors)
                    _logger.Info("Assembly types cached using Reflection. FullName = {0}", asm.FullName);
            }
            catch (ReflectionTypeLoadException e)
            {
                if (logErrors)
                    _logger.ErrorException(e, "Error while attempting to cache types using reflection. "
                    + "All dependencies are not loaded. Some types will not be cached. FullName = {0}", asm.FullName);

                missingDependencies = new List<string>();
                foreach (var dep in asm.GetReferencedAssemblies())
                    if (AppDomain
                        .CurrentDomain
                        .GetAssemblies()
                        .Where(a => a.FullName == dep.FullName)
                        .Count() == 0)
                    {
                        if (!String.IsNullOrEmpty(dep.FullName))
                            missingDependencies.Add(dep.FullName);
                        if (logErrors)
                            _logger.Info("Missing Dependency = {0}, Dependency of = {1}", dep.FullName, asm.FullName);
                    }

                types = e.Types;
            }
            return types;
        }

        public static AssemblyDefinition ReadCecilDefinition(byte[] assembly)
        {
            try
            {
                var asmStream = new MemoryStream(assembly);
                return AssemblyDefinition.ReadAssembly(asmStream);
            }
            catch (Exception e)
            {
                _logger.ErrorException(e, "Failed to convert byte array into assembly description.");
                return null;
            }
        }

        public static IEnumerable<TypeDefinition> ReadCecilTypes(byte[] assembly)
        {
            try
            {
                var asm = ReadCecilDefinition(assembly);
                return asm.MainModule.Types;
            }
            catch (Exception e)
            {
                _logger.ErrorException(e, "Failed to convert byte array into assembly description.");
                return null;
            }
        }

        #endregion

        #region Enumerable extensions

        public static void Iter<T>(this IEnumerable<T> collection, Action<T> action)
        {
            var temp = collection.ToList();
            foreach (var item in temp) action(item);
        }

        public static IEnumerable<S> TrySelect<T, S>(this IEnumerable<T> collection, Func<T, S> func)
        {
            var results = new List<S>();
            foreach (var item in collection)
                try { results.Add(func(item)); }
                catch (Exception) {}
            return results;
        }

        #endregion

        #region XML Read/Write

        public static T BinaryToXml<T>(byte[] data) where T : class
        {
            try
            {
                var serialiser = new XmlSerializer(typeof(T));
                T script;
                using (var fs = new MemoryStream(data))
                using (var reader = new XmlTextReader(fs))
                    script = (T)serialiser.Deserialize(reader);
                return script;
            }
            catch (Exception e)
            {
                _logger.ErrorException(e, "Failed to convert binary to XML.");
                return null;
            }
        }

        public static byte[] XmlToBinary<T>(T data) where T : class
        {
            try
            {
                var serialiser = new XmlSerializer(typeof(T));
                byte[] script;
                using (var fs = new MemoryStream())
                {
                    serialiser.Serialize(fs, data);
                    script = fs.ToArray();
                }
                return script;
            }
            catch (Exception e)
            {
                _logger.ErrorException(e, "Failed to convert binary to XML.");
                return null;
            }
        }

        public static string XmlToString<T>(T source) where T : class
        {
            try
            {
                var serializer = new XmlSerializer(typeof(T));
                var stringBuilder = new StringBuilder();
                using (var writer = XmlWriter.Create(stringBuilder))
                {
                    serializer.Serialize(writer, source);
                    return stringBuilder.ToString();
                }
            }
            catch (Exception e)
            {
                _logger.ErrorException(e, "Failed to convert binary to XML.");
                return "";
            }
        }

        public static bool XmlToFile<T>(T source, string fileName) where T : class
        {
            try
            {
                var serializer = new XmlSerializer(typeof(T));
                using (var writer = XmlWriter.Create(fileName))
                {
                    serializer.Serialize(writer, source);
                    return true;
                }
            }
            catch (Exception e)
            {
                _logger.ErrorException(e, "Failed to convert binary to XML.");
                return false;
            }
        }

        public static T ReadXml<T>(string fileName) where T : class
        {
            try
            {
                _logger.Info("Reading file = {0}", fileName);
                var serialiser = new XmlSerializer(typeof(T));
                T script;
                using (var fs = new FileStream(fileName, FileMode.Open))
                using (var reader = new XmlTextReader(fs))
                    script = (T)serialiser.Deserialize(reader);
                return script;
            }
            catch (Exception e)
            {
                _logger.ErrorException(e, "Error reading file as XML. FileName = " + fileName);
                return null;
            }
        }

        public static T StringToXml<T>(string scriptString) where T : class
        {
            try
            {
                var serialiser = new XmlSerializer(typeof(T));
                T script;
                using (var fs = new MemoryStream(Encoding.Default.GetBytes(scriptString)))
                using (var reader = new XmlTextReader(fs))
                    script = (T)serialiser.Deserialize(reader);
                return script;
            }
            catch (Exception e)
            {
                _logger.ErrorException(e, "Error converting string to XML.");
                return null;
            }
        }

        #endregion

        #region DataContract Read/Write

        public static T BinaryToDataContract<T>(byte[] data) where T: class
        {
            try
            {
                var serialiser = new DataContractSerializer(typeof(T));
                T script;
                using (var fs = new MemoryStream(data))
                using (var reader = new XmlTextReader(fs))
                    script = (T)serialiser.ReadObject(reader);
                return script;                
            } catch (Exception e)
            {
                _logger.ErrorException(e, "Failed to convert binary to XML.");
                return null;
            }
        }

        public static byte[] DataContractToBinary<T>(T data) where T : class
        {
            try
            {
                var serialiser = new DataContractSerializer(typeof(T));
                byte[] script;
                using (var fs = new MemoryStream())
                {
                    serialiser.WriteObject(fs, data);
                    script = fs.ToArray();
                }
                return script;
            }
            catch (Exception e)
            {
                _logger.ErrorException(e, "Failed to convert binary to XML.");
                return null;
            }
        }

        public static string DataContractToString<T>(T source) where T : class
        {
            try
            {
                var serializer = new DataContractSerializer(typeof(T));
                using (var backing = new StringWriter())
                using (var writer = new XmlTextWriter(backing))
                {
                    serializer.WriteObject(writer, source);
                    return backing.ToString();
                }
            }
            catch (Exception e)
            {
                _logger.ErrorException(e, "Failed to convert binary to XML.");
                return "";
            }
        }

        public static bool DataContractToFile<T>(T source, string fileName) where T : class
        {
            try
            {
                var serializer = new DataContractSerializer(typeof(T));
                using (var writer = XmlWriter.Create(fileName))
                {
                    serializer.WriteObject(writer, source);
                    return true;
                }
            }
            catch (Exception e)
            {
                _logger.ErrorException(e, "Failed to convert binary to XML.");
                return false;
            }
        }

        public static T ReadDataContract<T>(string fileName) where T : class
        {
            try 
            {
                _logger.Info("Reading file = " + fileName);
                var serialiser = new DataContractSerializer(typeof(T));
                T script;
                using (var fs = new FileStream(fileName, FileMode.Open))
                using (var reader = new XmlTextReader(fs))
                    script = (T)serialiser.ReadObject(reader);
                return script;
            }
            catch (Exception e)
            {
                _logger.ErrorException(e, "Error reading file as XML. FileName = " + fileName);
                return null;
            }
        }

        public static T StringToDataContract<T>(string scriptString) where T : class
        {
            try
            {
                var serialiser = new DataContractSerializer(typeof(T));
                T script;
                using (var fs = new MemoryStream(Encoding.Default.GetBytes(scriptString)))
                using (var reader = new XmlTextReader(fs))
                    script = (T) serialiser.ReadObject(reader);
                return script;
            }
            catch (Exception e)
            {
                _logger.ErrorException(e, "Error converting string to XML.");
                return null;
            }
        }

        #endregion

        #region File Read/Write

        public static bool WriteBytesToFile(string fileName, byte[] data)
        {
            try
            {
                _logger.Info("Writing new file. FileName = {0}", fileName);
                using (var fs = new FileStream(fileName, FileMode.Create))
                using (var bw = new BinaryWriter(fs))
                    bw.Write(data);
                return true;
            } catch (Exception e)
            {
                _logger.ErrorException(e, "Error writing data to file. FileName = " + fileName);
                return false;
            }
        }

        public static byte[] ReadBytesFromFile(string fileName)
        {
            if (String.IsNullOrEmpty(fileName)) return null;

            // Make path absolute
            fileName = Path.IsPathRooted(fileName)
                           ? fileName
                           : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);

            _logger.Info("Reading file '{0}'", fileName);
            try
            {
                byte[] buff = null;
                using (var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))
                using (var br = new BinaryReader(fs))
                {
                    long numBytes = new FileInfo(fileName).Length;
                    buff = br.ReadBytes((int)numBytes);
                }
                return buff;
            }
            catch (Exception e)
            {
                _logger.ErrorException(e, String.Format("Error reading file {0}.", fileName));
                return null;
            }
        }

        #endregion

        public static void SetField(this object obj, string fieldName, object value)
        {
            var field = obj.GetType().GetField(fieldName);
            if (field == null) return;
            var valueType = value.GetType();
            if (field.FieldType == valueType)
                field.SetValue(obj, value);
        }

    }
}
